package org.ddevil.data.util;

import java.text.NumberFormat;
import java.text.ParseException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;

import org.ddevil.data.Attribute;
import org.ddevil.data.BasicData;
import org.ddevil.data.BasicDisplayDescriptor;
import org.ddevil.data.Data;
import org.ddevil.data.DisplayDescriptor;
import org.ddevil.data.Filter;
import org.ddevil.data.StructureMutableData;
import org.ddevil.data.set.BasicDataSet;
import org.ddevil.data.set.DataSet;
import org.springframework.util.StringUtils;


/**
 * This class provides convienience methods for working with DataSets and other
 * related objects.
 *
 * @author Eric Lundin
 */
public class DataSetUtils {

	/**
	 * Get an array containing the data in the column specified. The array will
	 * be in the same order that the data set is in.
	 *
	 * @param data
	 *            The data set to operate on.
	 * @param dbIdentifier
	 *            The ID of the column.
	 * @return The data in the given column.
	 * @see DataSetInterface#getDataAt(int, String)
	 */
	public static Object[] getColumn(DataSet data, String columnId) {
		Object[] rv = new Object[data.size()];

		for (int i = 0; i < data.size(); i++) {
			rv[i] = data.getDataAt(i, columnId);
		}

		return rv;
	}

	/**
	 * A convenience for working with legacy code, which genneraly likes things
	 * as Strings.
	 *
	 * @see {@link DataSetUtils#getColumn(DataSet, String[])}
	 */
	public static String[] getColumnAsStrings(DataSet data, String columnId) {
		String[] rv = new String[data.size()];
		Object val;
		for (int i = 0; i < data.size(); i++) {
			val = data.getDataAt(i, columnId);
			rv[i] = (val != null ? val.toString() : null);
		}

		return rv;
	}

	/**
	 * Get a double array with all the data in the specified columns. The return
	 * array columns will be in the same order that the columns were specified
	 * as parameters. The return array rows will be in the same order that they
	 * are in the data set.
	 *
	 * @param data
	 *            The data set to operate on.
	 * @param dbIs
	 *            The IDs of the columns.
	 * @return The data in the given columns.
	 * @see DataSetInterface#getDataAt(int, String)
	 */
	public static Object[][] getColumns(DataSet data, String... columnIds) {
		Object[][] rv = new Object[data.size()][columnIds.length];

		for (int i = 0; i < data.size(); i++) {
			for (int j = 0; j < columnIds.length; j++) {
				rv[i][j] = data.getDataAt(i, columnIds[j]);
			}
		}

		return rv;
	}

	/**
	 * A convenience for working with legacy code, which genneraly likes things
	 * as Strings.
	 *
	 * @see {@link DataSetUtils#getColumns(DataSet, String[])}
	 */
	public static String[][] getColumnsAsStrings(DataSet data,
			String... columnIds) {
		String[][] rv = new String[data.size()][columnIds.length];
		Object val;
		for (int i = 0; i < data.size(); i++) {
			for (int j = 0; j < columnIds.length; j++) {
				val = data.getDataAt(i, columnIds[j]);
				rv[i][j] = (val != null ? val.toString() : null);
			}
		}

		return rv;
	}

	public static DataSet getColumnSubset(DataSet data, String... columnIds) {
		return new BasicDataSet(getColumns(data, columnIds), columnIds);
	}

	/**
	 * This utility method checks a column in a record group to see if the
	 * values are the same for each record in the group. If the column doesn't
	 * exist or there are no records in the group true is returned.<br>
	 * So if we has a Record Group with two columns identified a 'A' and 'B'
	 * with data that looked like:
	 *
	 * <pre>
	 *                                         'A'   'B'
	 *                                          1     1
	 *                                          1     2
	 * </pre>
	 *
	 * then,<br>
	 * isColumnDataTheSame(myGroup, "A") == true <br>
	 * isColumnDataTheSame(myGroup, "B") == false
	 *
	 * @param records
	 *            The group to perform the check on.
	 * @param columnId
	 *            The id of the column to check.
	 * @return true if the data is the same for the entire column.
	 */
	public static boolean isColumnDataTheSame(DataSet records, String columnId) {
		boolean result = true;
		Object[] columnData = getColumn(records, columnId);

		if (columnData != null && columnData.length > 0) {

			Object firstValue = columnData[0];
			for (Object currentValue : columnData) {
				if (!firstValue.equals(currentValue)) {
					result = false;
					break;
				}
			}
		}
		return result;
	}

	/**
	 * Checks a column in a record group to see if any of the data matches the
	 * value passed in.
	 *
	 * @param records
	 *            The Record Group to check.
	 * @param columnId
	 *            The DB Id of the column in the record group to check.
	 * @param value
	 *            The value to look for.
	 * @return true if any of the data in the column matches the value passed
	 *         in.
	 */
	public static boolean doesAnyDataInColumnMatch(DataSet records,
			String columnId, Object value) {
		boolean result = false;

		Object[][] columnData = getColumns(records, columnId);

		if (columnData != null && columnData.length > 0) {

			for (Object[] currentValue : columnData) {
				if (currentValue[0].equals(value)) {
					result = true;
					break;
				}
			}
		}

		return result;
	}

	/**
	 * Convert an {@link Data} object to an array that our CORBA
	 * objects will like. Each contained Data Item will be added as an array
	 * with the first element being it's DB Id and it's value.toString() being
	 * the second.
	 * <p>
	 * <strong>NOTE: If the value is <code>null</code> it will be converted to
	 * an empty String since QO's and BO's can't handle null attrib values.</strong>
	 *
	 * @param attribs
	 *            The Data object to build the array from.
	 * @return String double array containing each db id -> value pairing.
	 */
	public static String[][] attributesToArray(Data attribs) {
		String[][] rv = new String[attribs.size()][2];
		int i = 0;
		for (Attribute item : attribs) {
			rv[i][0] = item.getId();
			Object val = item.getValue();
			rv[i][1] = (val != null ? val.toString() : "");
			i++;
		}

		return rv;
	}

	/**
	 * Validate that the given Data Set has data mapped to each of the DB
	 * Identifiers contained in the given {@link DisplayDescriptor}. Every DB
	 * Id in this data set does not need to have a associated label in the
	 * display descriptor, but every DB Id that is labeled in the descriptor
	 * must be in this data set.
	 *
	 * @param dataSet
	 *            The DataSet to validate against.
	 * @param labels
	 *            The label map to validate.
	 * @return True if this is a valid label map for the given Data Set.
	 */
	public static boolean validateDisplayDescriptor(DataSet dataSet,
			DisplayDescriptor labels) {
		List<String> dbIdentifiers = dataSet.getIdentifiers();

		for (String id : labels.getAllIds()) {
			if (!dbIdentifiers.contains(id)) {
				return false;
			}
		}
		return true;
	}

	public static String formatAttribsTabular(Data in) {
		return formatDataSetTabular(new BasicDataSet(in));
	}

	/**
	 * Format a dataset for printing to the screen.
	 *
	 * @param ds
	 * @return
	 */
	public static String formatDataSetTabular(DataSet ds) {
		StringBuffer buf = null;
		if (ds.size() > 0) {
			Data rec = ds.exportRecord(0);
			List<String> idLookup = ds.getIdentifiers();
			int recSize = rec.size();

			// get the make col sizes
			int[] maxColSizes = new int[recSize];
			for (int i = 0; i < maxColSizes.length; i++) {
				maxColSizes[i] = 0;
			}

			// check each record
			for (int i = 0; i < ds.size(); i++) {
				for (int j = 0; j < recSize; j++) {
					Object val = ds.getDataAt(i, idLookup.get(j));
					maxColSizes[j] = Math.max(maxColSizes[j], val != null ? val
							.toString().length() : 4);
				}
			}

			// check the header
			for (int i = 0; i < idLookup.size(); i++) {
				maxColSizes[i] = Math.max(maxColSizes[i], idLookup.get(i)
						.length());
			}

			// determine total row length(last spacer included)
			int spacer = 4;
			int totalSize = 0;
			for (int col : maxColSizes)
				totalSize += col + spacer;

			// start buidling the string
			buf = new StringBuffer(totalSize * ds.size());

			// create the header
			int currentIndex = 0;
			for (int i = 0; i < idLookup.size(); i++) {
				String id = idLookup.get(i);
				buf.append(id);

				int nextIndex = currentIndex + maxColSizes[i] + spacer;
				currentIndex += id.length();
				while (currentIndex < nextIndex) {
					buf.append(" ");
					currentIndex++;
				}
			}
			buf.append("\n");
			for (int i = 0; i < totalSize; i++)
				buf.append("-");
			buf.append("\n");

			for (int row = 0; row < ds.size(); row++) {
				currentIndex = 0;
				for (int col = 0; col < recSize; col++) {
					Object val = ds.getDataAt(row, idLookup.get(col));
					String print = val != null ? val.toString() : "null";
					//  - commenting because its a dependency in the ui project
					// TODO extract this utility from windowkit
//					buf.append(FormatUtils.removeStringFormatting(print));
					int nextIndex = currentIndex + maxColSizes[col] + spacer;
					currentIndex += print.length();
					while (currentIndex < nextIndex) {
						buf.append(" ");
						currentIndex++;
					}
				}
				buf.append("\n");
			}

		}
		return buf != null ? buf.toString() : "DataSet empty.";

	}

	/**
	 * Compare two attribute objects for equality. Each attribute object must
	 * contain the same set of attribute ID's to be considered equal. The value
	 * of each attribute will be compared to its counterpart using
	 * {@link Object#equals(Object)}.
	 *
	 * @param first
	 * @param second
	 * @return
	 */
	public static boolean attribsEqual(Data first,
			Data second) {

		// check the obvious test first
		if (first.size() != second.size()) {
			return false;
		}

		boolean same = true;

		Object two;
		for (Attribute one : first) {
			if (second.contains(one.getId())) {
				two = second.getValue(one.getId());
				if (one.getValue() == null) {
					if (two != null) {
						// one is null two is not
						same = false;
						break;
					}
				} else if (!one.getValue().equals(two)) {
					// two attribs differ in value
					same = false;
					break;
				}
			} else {
				// first has a DB Id that second does not
				same = false;
				break;
			}
		}

		return same;
	}

	public static boolean attribsEqual(Data first,
			Data second, ArrayList<String> ids) {
		return attribsEqual(getAttributeSubset(first, ids
				.toArray(new String[0])), getAttributeSubset(second, ids
				.toArray(new String[0])));
	}

	/**
	 * Get a new attributes object that is a subset of the given object,
	 * containing only the ID's passed in. All attribute ID's passed in as
	 * parameters will be present in the return object, even if they were not in
	 * the attribute object passed in.<br>
	 * {@link BasicData} is the implementation used for the return object.
	 *
	 * @param attribs
	 * @param dbIds
	 * @return
	 */
	public static Data getAttributeSubset(Data attribs,
			String... dbIds) {
		BasicData ret = new BasicData(dbIds.length);
		for (String id : dbIds) {
			ret.setDataItem(id, attribs.getValue(id));
		}
		return ret;
	}

	public static Data getAttributeSubset(Data attribs,
			ArrayList<String> dbIds) {
		return getAttributeSubset(attribs, dbIds.toArray(new String[0]));
	}

	/**
	 * Retrieve the number of records in the given DataSet which match the given
	 * Data on the given key database identifiers.
	 *
	 * @param ds
	 *            The DataSet
	 * @param atts
	 *            The attributes to look for.
	 * @param keys
	 *            The ket database ids whose corresponding values in the
	 *            attributes and current dataset row must be equal to qualify a
	 *            match.
	 * @return
	 */
	public static int getMatchCount(DataSet ds, Data atts,
			List<String> keys) {
		// int count = 0;
		// boolean match;
		// for (int i = 0; i < ds.size(); i++) {
		// match = true;
		// for (String current : keys) {
		// if (!safeEquals(ds.getDataAt(i, current), atts
		// .getValue(current))) {
		// match = false;
		// break;
		// }
		// }
		// if (match) {
		// count++;
		// }
		// }
		return getMatchIndicies(ds, atts, keys).size();
	}

	/**
	 * Get the indicies of of data within the given DataSet which matches the
	 * attributes on the given key database identifiers.
	 *
	 * @param ds
	 * @param atts
	 * @param keys
	 * @return
	 */
	public static List<Integer> getMatchIndicies(DataSet ds,
			Data atts, List<String> keys) {
		boolean match;
		ArrayList<Integer> indicies = new ArrayList<Integer>();
		for (int i = 0; i < ds.size(); i++) {
			match = true;
			for (String current : keys) {
				if (!safeEquals(ds.getDataAt(i, current), atts
						.getValue(current))) {
					match = false;
					break;
				}
			}
			if (match) {
				indicies.add(i);
			}
		}
		return indicies;
	}

	/**
	 * Determine if two objects are equals with checks for null.
	 *
	 * @param val1
	 *            The first value.
	 * @param val2
	 *            The second value.
	 * @return True if both objects are equal, using the equals method of val1,
	 *         or both objects are null. False otherwise.
	 * @deprecated Use {@link DataUtils#safeEquals(Object, Object)}
	 */
	@Deprecated
	public static boolean safeEquals(Object val1, Object val2) {
		return DataUtils.safeEquals(val1, val2);
	}

	/**
	 * Get the index of the given attribute set within the dataset.
	 *
	 * @param ds
	 * @param atts
	 * @return
	 */
	public static int getIndexOfData(DataSet ds, Data atts) {
		int index = -1;
		boolean match;
		for (int i = 0; i < ds.size(); i++) {
			match = true;
			List<String> s = ds.getIdentifiers();
			for (String current : s) {
				if (!safeEquals(ds.getDataAt(i, current), atts
						.getValue(current))) {
					match = false;
				}
			}
			if (match) {
				index = i;
				break;
			}
		}
		return index;
	}

	/**
	 * Make the DataSet unique by removing any duplicate records.
	 *
	 * @param in
	 */
	public static void makeUnique(DataSet in) {
		List<String> ids = in.getIdentifiers();
		makeUnique(in, ids.toArray(new String[ids.size()]));
	}

	/**
	 * Make the DataSet unique by removing any duplicate records. Only the
	 * attributes passed in as keys will be used for checking for duplicates.
	 *
	 * @param in
	 * @param keys
	 */
	public static void makeUnique(DataSet in, String... keys) {

		for (int i = in.size() - 1; i >= 0; i--) {

			for (int j = i - 1; j >= 0; j--) {
				boolean match = true;
				for (String id : keys) {
					if (!safeEquals(in.getDataAt(i, id), in.getDataAt(j, id))) {
						match = false;
						break;
					}
				}
				if (match) {
					in.removeRecord(i);
					break;
				}
			}
		}
	}

	/**
	 * Test if the given DataSet has any duplicate records.
	 *
	 * @param in
	 * @return
	 */
	public static final boolean isSetUnique(DataSet in) {
		List<String> ids = in.getIdentifiers();
		return isSetUnique(in, ids.toArray(new String[ids.size()]));
	}

	/**
	 * Test if the given DataSet has any duplicate records. Only the attribute
	 * names passed in as keys will be used to check for duplicates.
	 *
	 * @param in
	 * @param keys
	 * @return
	 */
	public static final boolean isSetUnique(DataSet in, String... keys) {
		for (int i = in.size() - 1; i >= 0; i--) {

			for (int j = i - 1; j >= 0; j--) {
				boolean match = true;
				for (String id : keys) {
					if (!safeEquals(in.getDataAt(i, id), in.getDataAt(j, id))) {
						match = false;
						break;
					}
				}
				if (match) {
					return false;
				}
			}
		}
		return true;
	}

	/**
	 * Remove all attributes that have no value according to
	 * {@link DataUtils#hasValue(Object)}.
	 *
	 * @param in
	 *            The attributes object to prune.
	 */
	public static final void pruneAttributes(StructureMutableData in) {
		LinkedList<String> idsToGo = new LinkedList<String>();
		for (Attribute di : in) {
			if (!DataUtils.hasValue(di.getValue())) {
				idsToGo.add(di.getId());
			}
		}
		for (String id : idsToGo)
			in.removeDataItem(id);
	}

	/**
	 * Apply the record filter to each record in the DataSet. The actual Data
	 * Set passed in will be modified by having all the records that don't pass
	 * the fitler removed. The process starts with the last record first.
	 *
	 * @param in
	 *            The DataSet object to filter.
	 * @param filter
	 *            The record Filter to apply.
	 */
	public static final void filterDataSet(DataSet in,
			Filter<Data> filter) {
		for (int i = in.size() - 1; i >= 0; i--) {
			if (!filter.accept(in.exportRecord(i))) {
				in.removeRecord(i);
			}
		}

	}

	/**
	 * Filter the given DataSet by removing all records that don't have values
	 * for each of the specified attributes. Having a value is determined by
	 * {@link DataUtils#hasValue(Object)}.
	 *
	 * @param in
	 *            The DataSet to filter.
	 * @param attributes
	 *            The attributes that must contain values.
	 */
	public static final void filterDataSetNoValues(DataSet in,
			final String... attributes) {
		filterDataSet(in, new Filter<Data>() {
			public boolean accept(Data arg) {
				boolean accept = true;
				for (String attrib : attributes) {
					// if we find one of our attribs has no value this record
					// fails
					if (!DataUtils.hasValue(arg.getValue(attrib))) {
						accept = false;
						break;
					}
				}
				return accept;
			}
		});
	}

	/**
	 * Get the number of distinct key values are in this dataset.
	 *
	 * @param ds
	 * @param keys
	 * @return
	 */
	public static final int getUniqueCount(DataSet ds, String[] keys) {
		return getUniqueCount(ds, Arrays.asList(keys));
	}

	public static final int getUniqueCount(DataSet ds, List<String> keys) {
		int uniqueItems = 0;
		ArrayList<Integer> checkedRows;
		List<Integer> currentMatches;
		checkedRows = new ArrayList<Integer>();
		if (ds != null) {
			for (int i = 0; i < ds.size(); i++) {
				if (!checkedRows.contains(i)) {
					currentMatches = getMatchIndicies(ds, ds.exportRecord(i),
							keys);
					if (Collections.disjoint(currentMatches, checkedRows)) {
						uniqueItems++;
					}
					checkedRows.addAll(currentMatches);
				}
			}
		}
		return uniqueItems;
	}

	/**
	 * Return a new DisplayDescriptor object with the items sorted by label
	 * name.
	 *
	 * @param dd
	 *            The DisplayDescriptor to sort.
	 * @return A new DisplayDescriptor sorted by label name.
	 */
	public static final DisplayDescriptor sortDisplayDescriptor(DisplayDescriptor dd) {

		List<String> labels = dd.getAllLabels();
		Collections.sort(labels);
		BasicDisplayDescriptor ret = new BasicDisplayDescriptor(dd.size());
		for (String label : labels) {
			ret.setLabel(dd.getIDForLabel(label), label);
		}
		return ret;
	}

	public static double max(DataSet<? extends Data> data, String attribute) {
		double max = 0;
		for (int i = 0; i < data.size(); i++) {
			Object d = data.getDataAt(i, attribute);
			String ds = d != null ? d.toString() : "";
			if (StringUtils.hasText(ds)) {
				try {
					Number n = NumberFormat.getNumberInstance().parse(ds.trim());
					if (n.doubleValue() > max) {
						max = n.doubleValue();
					}
				} catch (ParseException e) {
					// TODO log
					System.out.println("Error parsing attribute " + attribute + ". Value was " + ds);
				}
			}
		}
		return max;
	}
	/**
	 * Calculate the average of the values in the column.
	 * @param data
	 * @param attribute
	 * @param ignoreEmpty if <code>true</code> then <code>null</code> values or values whose String
	 * from can't be parsed to a number will be ignored.  Otherwise they will be calculated into
	 * the average as <code>0</code>
	 * @return
	 */
	public static double avg(DataSet<? extends Data> data, String attribute, boolean ignoreEmpty) {
		double sum = 0;
		int samples = 0;
		for (int i = 0; i < data.size(); i++) {
			Object d = data.getDataAt(i, attribute);
			String ds = d != null ? d.toString().trim() : "";
			double val = 0;
			if (StringUtils.hasText(ds)) {
				try {
					Number n = NumberFormat.getNumberInstance().parse(ds);
					val = n.doubleValue();
				} catch (ParseException e) {
					// TODO log
					System.out.println("Error parsing attribute " + attribute + ". Value was " + ds);
					if (ignoreEmpty) {
						continue;
					}
				}
			}
			else {
				if (ignoreEmpty) {
					continue;
				}
			}
			sum += val;
			samples++;
		}
		return samples == 0 ? 0 : sum / samples;
	}

}
